Skip to content
86 linesCodeBlameRaw
1import { Stack, router, useLocalSearchParams } from "expo-router";
2import * as WebBrowser from "expo-web-browser";
3import { ExternalLink, Send } from "lucide-react-native";
4import { useState } from "react";
5import { View } from "react-native";
6
7import { Body, IssueIcon, PullIcon, Timeline } from "@/components/code";
8import { Button, ErrorView, Field, Loading, Row, Screen, SectionHeader, Text } from "@/components/ui";
9import { useComment, useIssue } from "@/lib/code";
10import { siteUrl } from "@/lib/server";
11import { useSignedIn } from "@/lib/session";
12import { ago } from "@/lib/time";
13import { space } from "@/theme";
14
15/** An issue: what it asks for, the pull requests made for it, and its conversation. */
16export default function Issue() {
17 const params = useLocalSearchParams<{ owner: string; repo: string; number: string }>();
18 const number = Number(params.number);
19 const { server } = useSignedIn();
20 const issue = useIssue(params.owner, params.repo, number);
21 const comment = useComment(params.owner, params.repo, number);
22 const [draft, setDraft] = useState("");
23
24 if (issue.isPending || issue.error) {
25 return (
26 <Screen>
27 <Stack.Screen options={{ title: `#${number}` }} />
28 {issue.error ? <ErrorView error={issue.error} retry={() => issue.refetch()} /> : <Loading />}
29 </Screen>
30 );
31 }
32
33 const { issue: item, pulls, comments } = issue.data;
34 const by = item.author.display_username ?? item.author.username;
35
36 return (
37 <Screen scroll>
38 <Stack.Screen options={{ title: `#${number}` }} />
39 <View style={{ gap: space.sm }}>
40 <Text size="heading">{item.title}</Text>
41 <View style={{ flexDirection: "row", gap: space.sm, alignItems: "center" }}>
42 <IssueIcon issue={item} />
43 <Text size="small" tone="muted">
44 {item.state === "open" ? "Open" : "Closed"} · {by} · {ago(item.created_at)}
45 {item.labels.length ? ` · ${item.labels.join(", ")}` : ""}
46 </Text>
47 </View>
48 </View>
49 <Body text={item.body} />
50 {pulls.length ? (
51 <View style={{ marginHorizontal: -space.lg }}>
52 <SectionHeader title="Pull requests" />
53 {pulls.map((pull) => (
54 <Row
55 key={pull.id}
56 leading={<PullIcon pull={pull} />}
57 title={pull.title}
58 subtitle={`#${pull.number} · ${pull.status}`}
59 onPress={() =>
60 router.push({ pathname: "/code/[owner]/[repo]/pull/[number]", params: { owner: params.owner, repo: params.repo, number: String(pull.number) } })
61 }
62 />
63 ))}
64 </View>
65 ) : null}
66 <Timeline comments={comments} />
67 <View style={{ gap: space.sm }}>
68 <Field label="Comment" placeholder="Add to the conversation" value={draft} onChangeText={setDraft} multiline />
69 <Button
70 title="Comment"
71 icon={Send}
72 busy={comment.isPending}
73 disabled={!draft.trim()}
74 onPress={() => comment.mutate(draft.trim(), { onSuccess: () => setDraft("") })}
75 />
76 {comment.error ? <Text tone="danger">{comment.error.message}</Text> : null}
77 </View>
78 <Button
79 title="Open on g1t"
80 variant="secondary"
81 icon={ExternalLink}
82 onPress={() => WebBrowser.openBrowserAsync(siteUrl(server, `/${params.owner}/${params.repo}/issues/${number}`))}
83 />
84 </Screen>
85 );
86}